单例模式或许是最简单的设计模式之一,也很常见,但其实现还有许多需要注意的地方。
What
顾名思义,单例模式在整个JVM的运行期间为单例的类只创建一个实例。
因此不难想象有两点需要保证:
- 单例的创建不能任由代码随意执行,因此单例类的构造方法应该是private的。
- 单例的创建应该只有一次,时机应该在第一次调用该单例对象的方法前。
创建方式
从实例创建的时机来划分,单例模式有两种创建方式。
饿汉
所谓饿汉式指的是在类加载阶段就将单例类的实例创建出来,不管以后有没有用到,优点是在以后的JVM运行期不会再有创建实例的动作,因此基本可以保证单例的唯一性;缺点是当没有用到时会造成浪费。
实现饿汉式的单例很简单,只需持有静态的自身单例类对象,限制构造方法,并提供静态的实例获取方法即可,同时最好用final修饰该单例对象。1
2
3
4
5
6public class Singleton {
private final static Singleton INSTANCE = new Singleton();
private Singleton(){}
public static Singleton getInstance() {
return INSTANCE;
}
懒汉
与饿汉相对,懒汉模式只有当调用该单例的方法时,才涉及到该实例的创建,即延迟加载。优点是由于按需创建实例,不会造成浪费;缺点是难以控制单例唯一性,因为需保证只有第一次获取实例时才创建实例,在多线程环境下需特殊处理。
针对懒汉式单例的唯一性控制,通常有如下几种方式。
方法锁
我们可以用synchronized关键字修饰获取单例实例的静态方法,这样所有获取单例对象的线程都处于竞争态,每次只有一个线程进入该方法,其他线程将等待锁的释放,因此只有第一个拿到该锁的方法会创建实例。1
2
3
4
5
6
7
8
9
10public class Singleton {
private static Singleton INSTANCE;
private Singleton(){}
public synchronized static Singleton getInstance() {
if(null == INSTANCE) {
INSTANCE = new Singleton();
}
return INSTANCE;
}
}
但是用synchronized修饰方法的粒度实在是太大了,可能会造成效率低下。
判断锁
那么如果把锁的粒度下降,把锁下移到方法内的块中会不会好点?下面这种实现看起来很自然:1
2
3
4
5
6
7
8
9
10
11
12public class Singleton {
private static Singleton INSTANCE;
private Singleton(){}
public static Singleton getInstance() {
if(null == INSTANCE) {
synchronized(Singleton.class) {
INSTANCE = new Singleton();
}
}
return INSTANCE;
}
}
初看似乎没毛病,但仔细推敲就能发现问题:
- T1进入判断,获取锁,创建一个实例并将INSTANCE引用指向该对象。
- T2同时进入判断,等待T1释放锁后获取锁,也创建一个实例,将INSTANCE指向新对象。
- T3稍后进行判断,发现INSTANCE不为空,直接返回T2创建的实例。
由此可见,只有一次判断内的锁并不能保证单例,因此我们需要对其进行一些小修改:
1 | class Singleton { |
这样的话,上述情况就没问题了:
- T1进入判断,获取锁,创建一个实例并将INSTANCE引用指向该对象。
- T2同时进入判断,等待T1释放锁后获取锁,这时进行第二次判断,直接返回不为空的INSTANCE。
- T3稍后进行判断,发现INSTANCE不为空,直接返回T1创建的实例。
至于为什么要使用volatile关键字,可以参考线程同步之volatile,注意volatile只有在Java 5+才算完善。
静态内部类
当一个类中包含静态内部类时,由于两个类没有继承关系,因此在加载外部类时,并不会加载内部类,只有当调用其方法时才会加载,而JVM的类加载能保证线程安全,因此可以用静态内部类实现懒汉式单例。1
2
3
4
5
6
7
8
9
10public class Singleton {
private Singleton(){}
public static Singleton getInstance() {
return SingletonHolder.INSTANCE;
}
public static class SingletonHolder {
private SingletonHolder(){}
private static Singleton INSTANCE = new Singleton();
}
}
反射大魔王
然而,不管是饿汉也好,懒汉也罢,都将体验被反射大魔王支配的恐惧,让我们看看反射如何一招秒全场。1
2
3
4
5
6
7
8
9
10
11public static void main(String[] args) {
try {
Singleton s1 = Singleton.getInstance();
Constructor<Singleton> constructor = Singleton.class.getDeclaredConstructor();
constructor.setAccessible(true);
Singleton s2 = constructor.newInstance();
System.out.println(s1 == s2);
} catch (Exception e) {
e.printStackTrace();
}
}
可以观察到,s1和s2指向不同的对象。
只需要通过反射将构造方法的访问权限放开,锁、静态内部类之流全部失效。
枚举守护神
当然,反射也不是万能的,可以采取一些措施来防范,如给实例化添加次数记录,超过1次则抛出异常。但这些方法都需要添加代码逻辑去处理,如果用枚举来实现单例,可以利用JVM的机制去抵御破坏。
枚举 Enum
其实是一个抽象类,当程序代码中声明一个枚举其实继承了Enum
。
如下是利用枚举实现单例模式。1
2
3
4public enum Singleton {
INSTANCE;
private Singleton(){}
}
这个方式是饿汉式的,在类加载阶段就会创建单例;同时由于在类加载阶段就创建了对象,又能保证线程安全。
使用之前的反射手段尝试改变构造方法权限,调用constructor
的newInstance
方法时,首先没有无参构造方法,只有带两个参数的构造方法(String.class, int.class)
(即name和ordinal属性,分别对应枚举名字及在枚举中的位置);其次调用时,如发现是枚举类则抛出IllegalArgumentException
异常,这是由Java的语言规则限制的。至此,枚举单例成功抵御了反射直接调用构造方法的进攻。